# -*- coding: utf-8 -*-
require "jdbc_migration"
module JdbcMigration
  class RailsMigrator
    module CreatableWithId
      def create_with_id(id, attrs)
        result = self.new(attrs)
        result[:id] = id
        result.save!
      end
    end
    
    def initialize(migration, options = nil)
      @migration = migration
      @options = {
          :force_id => true,
          :rename_pk_to_id => true,
          :pluralize_table_name => true,
          :uniq_keys_for_composite_primary_keys => true,
          :allowed_pk_jdbc_types => %w(NUMERIC ROWID TINYINT SMALLINT INTEGER BIGINT),
          :no_limit_rails_types => [:primary_key, :boolean, :date, :datetime, :time, :timestamp]
        }.update(options || {})
      @table_schema_patterns = []
      @column_schema_patterns = []
      @record_patterns = []
    end
    
    def prepare_schema_patterns(*tables, &block)
      each(tables) do |table|
        table.pluralize_table_name = @options[:pluralize_table_name]
        table_options = {
            :id => (/^id$/i =~ table.primary_key) ? true : # pkがもともとidならtrue
              !!table.primary_key ? 
              @options[:rename_pk_to_id] : # pkが1つだけ存在して、かつidにリネームするならtrue
              @options[:force_id] # pkが複数存在、あるいは存在しない場合に、:force_idが指定されてたらtrue
          }
        table.rails_table_name, table.rails_options = filter_table_by_patterns(table, table_options)

        table.columns.each do |col|
          col.rails_ignored = 
            (col.primary_key? and !!table.primary_key) ||
            (col.primary_key? and (/^id$/i =~ table.primary_key)) ||
            (!col.primary_key? and (/^id$/i =~ col.name))
          next if col.rails_ignored
          
          column_options = {
            :default => col.default,
            :null => col.nullable?
          }
          unless @options[:no_limit_rails_types].include?(col.rails_type)
            column_options.update(
              :limit => col.size,
              :precision => col.decimal_digits
              )
          end
          col.rails_name, col.rails_type, col.rails_options = filter_column_by_patterns(col, column_options)
        end
      end
    end
    
    def process_schema(*tables, &block)
      each(tables) do |table|
        verbose = "create_table(#{table.rails_table_name.inspect}, #{table.rails_options.inspect}) do |t|\n"
        begin
          @migration.create_table(table.rails_table_name, table.rails_options) do |t|
            table.columns.each do |col|
              next if col.rails_ignored
              verbose << "  t.column('#{col.rails_name}', #{col.rails_type.inspect}, #{col.rails_options.inspect})\n"
              t.column(col.rails_name, col.rails_type, col.rails_options)
            end
          end
          verbose << "end"
        rescue Exception => err
          raise err
        ensure
          puts verbose
        end
      end
    end

    def filter_table_by_patterns(table, table_options)
      @table_schema_patterns.each do |(matcher, filter_or_result)|
        if matching?(matcher, table, table_options)
          actual_result = {:name => table.rails_table_name.downcase, :options => table_options}
          result = filter_or_result.respond_to?(:call) ? 
            filter.call(table, table_options) : filter_or_result
          if result.is_a?(Hash)
            actual_result.update(result)
            return actual_result[:name], actual_result[:options]
          elsif result.is_a?(String)
            return result, actual_result[:options]
          else
            raise ArgumentError, "Invalid filter return #{result.inspect}. Must be a String or a Hash which has :name or :options"
          end
        end
      end
      return table.rails_table_name, table_options
    end
    
    def filter_column_by_patterns(col, column_options)
      @column_schema_patterns.each do |(matcher, filter_or_result)|
        if matching?(matcher, col, column_options)
          actual_result = {:name => col.name.downcase, :type => col.rails_type, :options => column_options}
          result = filter_or_result.respond_to?(:call) ? 
            filter_or_result.call(col, column_options) : filter_or_result
          if result.is_a?(Hash)
            actual_result.update(result)
            return actual_result[:name], actual_result[:type], actual_result[:options]
          elsif result.is_a?(String)
            return result, actual_result[:type], actual_result[:options]
          else
            raise ArgumentError, "Invalid filter return. Must be a Hash which has :name, :type or :options"
          end
        end
      end
      return col.name.downcase, col.rails_type, column_options
    end
    
    def process_data(*tables, &block)
      each(tables) do |table|
        jdbc_model = table.define_jdbc_model(@migration)
        rails_model = table.define_rails_model(@migration)
        $stdout.puts("-- copying records from %s to %s" % [jdbc_model.table_name, rails_model.table_name])
        rails_model.extend(CreatableWithId)
        rails_model.transaction do
          id_exist = !!table.primary_key
          select = "select * from #{jdbc_model.table_name}"
          select << " order by #{table.primary_key}" if id_exist
          records = jdbc_model.connection.select_all(select)
          records.each do |record|
            filter_record_by_schema_patterns(table, record)
            filter_record_by_patterns(table, record)
            if id_exist
              rails_model.create_with_id(record.delete(table.primary_key.downcase), record)
            else
              rails_model.create(record)
            end
          end
        end
      end
    end
    
    def filter_record_by_schema_patterns(table, record)
      table.columns.each do |col|
        if col.rails_ignored
          record.delete(col.name.to_s.downcase)
        else
          record[col.rails_name] = record.delete(col.name.to_s.downcase)
        end
      end
    end
    
    def filter_record_by_patterns(table, record)
      @record_patterns.each do |record_pattern|
        matcher, filter = *record_pattern
        if matching?(matcher, table, record)
          filter.call(table, record)
        end
      end
    end
  
    def each(tables, &block)
      tables.each do |table|
        begin
          yield(table)
        rescue Exception => err
          require "pp"
          pp table
          raise err
        end
      end
    end

    def add_table_schema_pattern(matcher = true, result = nil, &filter)
      if block_given?
        @table_schema_patterns << [matcher, filter]
      elsif result
        @table_schema_patterns << [matcher, result]
      else
        raise ArgumentError, "add_table_schema_pattern requires a block" 
      end
    end
    
    def add_column_schema_pattern(matcher = true, result = nil, &filter)
      if block_given?
        @column_schema_patterns << [matcher, filter]
      elsif result
        @column_schema_patterns << [matcher, result]
      else
        raise ArgumentError, "add_column_schema_pattern requires a block"
      end
    end
    
    def add_record_pattern(matcher = true, &filter)
      raise ArgumentError, "add_record_pattern requires a block" unless block_given?
      @record_patterns << [matcher, filter]
    end
    
    def add_cast_record_time(matcher = true)
      add_record_pattern(matcher, &method(:cast_record_time))
    end
    
    def add_cast_record_date(matcher = true)
      add_record_pattern(matcher, &method(:cast_record_date))
    end
    
    def cast_record_time(table, record)
      table.columns.each do |col|
        if col.rails_type == :time
          value = record[col.name.downcase]
          record[col.name.downcase] = Time.parse(value).strftime("%Y/%m/%d %H:%M:%S") if value
        end
      end
    end
    
    def cast_record_date(table, record)
      table.columns.each do |col|
        if col.rails_type == :date
          value = record[col.name.downcase]
          record[col.name.downcase] = Date.parse(value).strftime("%Y/%m/%d") if value
        end
      end
    end
    
    private
    def matching?(matcher, named_object, *args)
      matcher.nil? ? false :
      matcher == false ? false :
      matcher == true ? true :
      matcher.is_a?(Regexp) ? matcher =~ named_object.name :
      (matcher.is_a?(String) or matcher.is_a?(Symbol)) ? 
        matcher.to_s.downcase == named_object.name.to_s.downcase :
      matcher.respond_to?(:call) ? matcher.call(named_object, *args) : false
    end
    
  end
end
